《HTTP 权威指南》: 实体和编码

1. 报文是箱子,实体是货物

HTTP 实体首部描述了 HTTP 报文的内容,HTTP/1.1 版定义了以下 10 个基本字体首部字段:

  • Content-Type:实体中所承载的对象类型;
  • Content-Length:所传送实体主体的长度或大小;
  • Content-Language:与所传送对象最相配的人类语言;
  • Content-Encoding:对象数据所做的任意变换;
  • Content-Location:一个备用位置,请求时可通过它获得对象;
  • Content-Range:如果这是部分实体,这个首部说明它是整体的哪个部分;
  • Content-MD5:实体主体内容的校验和;
  • Content-Modified:所传输内容在服务器上创建或最后修改的日期时间;
  • Expires:实体数据将要失效的日期时间;
  • Allow:该资源所允许的各种请求方法;
  • ETag:这份文档特定实例的唯一验证码;
  • Cache-Control:指出应该如何缓存该文档。

2. Content-Length:实体的大小

Content-Length 首部指示出报文中实体主体的字节大小。这个大小是包含了所有内容编码的。比如,对文本文件进行了 gzip 压缩的话,Content-Length 首部就是压缩后的大小,而不是原始大小。

除非使用了分块编码,否则 Content-Length 首部就是带有实体主体的报文必须使用的。使用 Content-Length 首部是为了能够检测出服务器崩溃而导致的报文截尾,并对共享持久连接的多个报文进行正确分段。

HTTP 的早期版本采用关闭连接的办法来划定报文的结束。但是,没有 Content-Length 的话,客户端无法区分到底是报文结束时正常的连接关闭,还是报文传输中由于服务器崩溃而导致的连接关闭。客户端需要通过 Content-Length 来检测报文截尾。

Content-Length 首部对于持久连接是必不可少的。如果响应通过持久连接传送,就可能有另一条HTTP响应紧随其后。客户端通过 Content-Length 首部就可以知道报文在何处结束,下一条报文从何处开始。因为连接是持久的,客户端无法依赖连接关闭来判别报文的结束。如果没有 Content-Length 首部,HTTP 应用程序就不知道某个实体主体在哪里结束,下一条报文从哪里开始。

HTTP 允许对实体主体的内容进行编码,比如可以使之更安全或进行压缩以节省空间。如果主体进行了内容编码,Content-Length 首部说明的就是编码后(encoded)的主体的字节长度,而不是未编码的原始主体的长度。

下面列出的规则说明了在若干不同的情况下如何正确计算主体的长度和结束位置。这些规则应当按顺序应用,谁先匹配就用谁。

  • 如果特定的 HTTP 报文类型中不允许带有主体,就忽略 Content-Length 首部,它是对没有实际发送出来的主体进行计算的。这种情况下,Content-Length 首部是提示性的,并不说明实际的主体长度。
  • 如果报文中含有描述传输编码的 Transfer-Encoding 首部(不采用默认的 HTTP “恒等” 编码),那实体就应由一个称为 “零字节块”(zero-byte chunk)的特殊模式结束,除非报文已经因连接关闭而结束。
  • 如果报文中含有 Content-Length 首部(并且报文类型允许有实体主体),而且没有非恒等的 Transfer-Encoding 首部字段,那么 Content-Length 的值就是主体的长度。如果收到的报文中既有 Content-Length 首部字段又有非恒等的 Transfer-Encoding 首部字段,那就必须忽略 Content-Length,因为传输编码会改变实体主体的表示和传输方式(因此可能就会改变传输的字节数)。
  • 如果报文使用了 multipart/byteranges(多部分/字节范围)媒体类型,并且没有用 Content-Length 首部指出实体主体的长度,那么多部分报文中的每个部分都要说明它自己的大小。这种多部分类型是唯一的一种自定界的实体主体类型,因此除非发送方知道接收方可以解析它,否则就不能发送这种媒体类型。
  • 如果上面的规则都不匹配,实体就在连接关闭的时候结束。实际上,只有服务器可以使用连接关闭来指示报文的结束。客户端不能用关闭连接来指示客户端报文的结束,因为这样会使服务器无法发回响应。

3. 内容编码

内容编码的过程如下所述

  1. 网站服务器生成原始响应报文,其中有原始的 Content-Type 和 Content- Length 首部。
  2. 内容编码服务器(也可能就是原始的服务器或下行的代理)创建编码后的报文。编码后的报文有同样的 Content-Type 但 Content-Length 可能不同(比如主体被压缩了)。内容编码服务器在编码后的报文中增加 Content-Encoding 首部,这样接收的应用程序就可以进行解码了。
  3. 接收程序得到编码后的报文,进行解码,获得原始报文。

为了避免服务器使用客户端不支持的编码方式,客户端就把自己支持的内容编码方式列表放在请求的 Accept-Encoding 首部里发出去。如果 HTTP 请求中没有包含 Accept-Encoding 首部,服务器就可以假设客户端能够接受任何编码方式(等价于发送 Accept-Encoding:*)。

4. 传输编码和分块编码

4.1 Transfer-Encoding 首部

HTTP 协议中只定义了下面两个首部来描述和控制传输编码

  • Transfer-Encoding:告知接收方为了可靠地传输报文,已经对其进行了何种编码。
  • TE:用在请求首部中,告知服务器可以使用哪些传输编码扩展。

4.2 分块编码

分块编码把报文分割为若干个大小已知的块。块之间是紧挨着发送的,这样就不需要在发送之前知道整个报文的大小了。要注意的是,分块编码是一种传输编码,因此是报文的属性,而不是主体的属性。

若客户端和服务器之间不是持久连接,客户端就不需要知道它正在读取的主体的长度,而只需要读到服务器关闭主体连接为止。当使用持久连接时,在服务器写主体之前,必须知道它的大小并在 Content-Length 首部中发送。如果服务器动态创建内容,就可能在发送之前无法知道主体的长度。

分块编码为这种困难提供了解决方案,只要允许服务器把主体逐块发送,说明每块的大小就可以了。因为主体是动态创建的,服务器可以缓冲它的一部分,发送其大小和相应的块,然后在主体发送完之前重复这个过程。服务器可以用大小为 0 的块作为主体结束的信号,这样就可以继续保持连接,为下一个响应做准备。

分块编码是相当简单的。它由起始的 HTTP 响应首部块开始,随后就是一系列分块。每个分块包含一个长度值和该分块的数据。长度值是十六进制形式并将 CRLF 与数据分隔开。分块中数据的大小以字节计算,不包括长度值与数据之间的 CRLF 序列以及分块结尾的 CRLF 序列。最后一个块有点特别,它的长度值为 0,表示 “主体结束”。